/**
 * Copyright European Commission's
 * Taxation and Customs Union Directorate-General (DG TAXUD).
 */
package com.eryos.selenium;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeUnit;

import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxProfile;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;

/**
 * 
 * @author $Author: $
 * @version $Revision: $
 */
public abstract class SeleniumTestCase {

	private static final int TIMEOUT = 30;
	private static final int STEP_WAIT_TIME = 500;
	private static final String BASE_URL = "http://fd2ts-vm:7001/";
	protected boolean quit = true;

	enum Driver {
		Firefox, Chrome, IE;
	}

	protected RemoteWebDriver driver;
	private String baseUrl;
	private boolean acceptNextAlert = true;
	private final StringBuffer verificationErrors = new StringBuffer();

	boolean highlighted = true;

	private String testName = "";

	/**
	 * 
	 * Default constructor.
	 */
	public SeleniumTestCase() {
		getLogger()
				.info("----------------------------------------------------");
		getLogger().info("Create new Selenium test case");
		getLogger().info("BaseUrl : {}", BASE_URL);
		getLogger().info("Timeout (sec) : {}", TIMEOUT);
	}

	Capabilities actualCapabilities;

	private final List<String> tags = new ArrayList<String>();

	public void addTag(String tag) {
		tags.add(tag);
	}

	private void setSauceLabsInformation(final DesiredCapabilities capabilities) {

		String name = System.getProperty("selenium.saucelabs.name");
		if (name == null || name.isEmpty()) {
			capabilities.setCapability("name", name + " : " + testName);
		} else {
			capabilities.setCapability("name", testName);
		}
		getLogger().info("Saucelabs name : {}",
				capabilities.getCapability("name"));

		String build = System.getProperty("selenium.saucelabs.build");
		if (build == null || build.isEmpty()) {
			capabilities.setCapability("build", build);
		} else {
			capabilities.setCapability("build", "0.0.0");
		}
		getLogger().info("Saucelabs build : {}",
				capabilities.getCapability("build"));

		capabilities.setCapability("tags", tags.toArray(new String[0]));
		getLogger().info("Saucelabs tags : {}",
				capabilities.getCapability("tags"));

		// SauceLabs information capabilities.setCapability("record-video",
		// "true"); // SauceLabs information
		/*
		 * capabilities.setCapability("video-upload-on-pass", "false"); //
		 * SauceLabs information
		 * capabilities.setCapability("record-screenshots", "false"); //
		 * SauceLabs information capabilities.setCapability("capture-html",
		 * "false"); // SauceLabs information
		 * capabilities.setCapability("video-upload-on-pass", "false"); //
		 * SauceLabs information
		 */

	}

	/**
	 * Call before all TC.
	 * 
	 * @throws Exception
	 */
	@Before
	public void setUp() throws Exception {
		getLogger().info("setUp");

		// We could use any driver for our tests...
		DesiredCapabilities capabilities = new DesiredCapabilities();

		// Retrieve param
		Driver browser = Driver.Firefox;
		String browserParam = System.getProperty("selenium.browser");
		String browserVersionParam = System
				.getProperty("selenium.browser.version");
		String host = System.getProperty("selenium.server.host");
		String port = System.getProperty("selenium.server.port");

		// Check default values
		if (browserParam != null) {
			if (Driver.Chrome.name().equalsIgnoreCase(browserParam)) {
				browser = Driver.Chrome;
			} else if (Driver.IE.name().equalsIgnoreCase(browserParam)) {
				browser = Driver.IE;
			} else {
				browser = Driver.Firefox;
			}
		}

		if (host == null || host.isEmpty()) {
			host = "localhost";
		}
		if (port == null || port.isEmpty()) {
			port = "4444";
		}

		switch (browser) {
		case Chrome:
			System.setProperty("webdriver.chrome.driver",
					"t:\\chromedriver.exe");
			capabilities = DesiredCapabilities.chrome();
			capabilities.setBrowserName(DesiredCapabilities.chrome()
					.getBrowserName());
			break;
		case IE:
			capabilities = DesiredCapabilities.internetExplorer();
			capabilities.setBrowserName(DesiredCapabilities.internetExplorer()
					.getBrowserName());
			break;
		case Firefox:
		default:
			FirefoxProfile profile = new FirefoxProfile();
			profile.setEnableNativeEvents(true);
			capabilities = DesiredCapabilities.firefox();
			capabilities.setBrowserName(DesiredCapabilities.firefox()
					.getBrowserName());
			capabilities.setCapability(FirefoxDriver.PROFILE, profile);
		}

		capabilities.setJavascriptEnabled(true);

		// Set browser version
		if (browserVersionParam != null && !browserVersionParam.isEmpty()) {
			capabilities.setVersion(browserVersionParam);
		}

		String hubUrl = System.getProperty("selenium.hub.url");
		if (hubUrl == null || hubUrl.isEmpty()) {
			hubUrl = "http://" + host + ":" + port + "/wd/hub";
		}

		setSauceLabsInformation(capabilities);

		// Log
		getLogger().info("Browser : {} (v{})", browser, browserVersionParam);
		getLogger().info("Selenium hubUrl : {}", hubUrl);

		// Get a handle to the driver. This will throw an exception
		// if a matching driver cannot be located
		driver = new RemoteWebDriver(new URL(hubUrl), capabilities);

		// Query the driver to find out more information
		actualCapabilities = driver.getCapabilities();

		// And now use it
		baseUrl = BASE_URL;
		driver.manage().timeouts().implicitlyWait(TIMEOUT, TimeUnit.SECONDS);
	}

	private void waitForelement(final By by) {

		WebDriverWait wait = new WebDriverWait(driver, TIMEOUT / 2);
		wait.until(ExpectedConditions.presenceOfElementLocated(by));

	}

	private WebElement findElement(final By by) {

		WebDriverWait wait = new WebDriverWait(driver, TIMEOUT / 2);
		return wait.until(ExpectedConditions.presenceOfElementLocated(by));

	}

	private boolean isElementPresentWithTimeout(final By by) {

		try {
			waitForelement(by);
			return true;
		} catch (TimeoutException e) {
			return false;
		}

	}

	protected void restartBrowser() {
		try {

			String url = driver.getCurrentUrl();

			driver.close();

			setUp();

			driver.get(url);

		} catch (Throwable e) {
			e.printStackTrace();
		}
	}

	/**
	 * Call after each TC.
	 * 
	 * @throws Exception
	 */
	@After
	public void tearDown() throws Exception {
		getLogger().info("tearDown");
		String source = driver.getPageSource();
		String verificationErrorString = verificationErrors.toString();
		if (quit) {
			driver.quit();
			if (!"".equals(verificationErrorString)) {
				getLogger().error(verificationErrorString);
				getLogger().error(source);

				File scrFile = ((TakesScreenshot) driver)
						.getScreenshotAs(OutputType.FILE);
				// Now you can do whatever you need to do with it, for example
				// copy somewhere
				FileUtils.copyFile(scrFile, new File("screenshot.png"));

				fail(verificationErrorString);
			}
		}
	}

	public abstract Logger getLogger();

	/**
	 * 
	 * @param by
	 *            Element locator
	 */
	public void click(final By by) {
		getLogger().debug("click <{}>", by);
		assertElementPresent(by);

		WebElement element = this.findElement(by);
		highlightElementRed(element);
		element.click();

	}

	/**
	 * 
	 * @param title
	 */
	public void checkTitle(final String title) {
		getLogger().debug("checkTitle {}", title);
		assertEquals(title, getDriver().getTitle());
	}

	/**
	 * 
	 * @param text
	 */
	public void assertTextPresent(final String text) {
		getLogger().debug("assertTextPresent {}", text);
		try {
			WebElement element = findElement(By.cssSelector("BODY"));
			Assert.assertTrue(element.getText().matches(text));
		} catch (Error e) {
			getLogger().error("Text not present <{}>", text);
			Assert.fail("Text not present : " + text);
		}
	}

	/**
	 * 
	 * @param text
	 */
	public void assertTextNotPresent(final String text) {
		getLogger().debug("assertTextNotPresent {}", text);
		try {
			WebElement element = findElement(By.cssSelector("BODY"));
			Assert.assertFalse(element.getText().matches(text));
		} catch (Error e) {
			Assert.fail("Error while searching text : " + text);
		}
	}

	/**
	 * Assert that an element is present on the page
	 * 
	 * @param by
	 *            Element locator
	 */
	public void assertElementPresent(final By by) {
		getLogger().debug("assertElementPresent {}", by);
		try {
			assertTrue(isElementPresent(by));
		} catch (Error e) {
			getLogger().error("Element not present <{}>", by);
			verificationErrors.append(e.toString());
			fail("Element <" + by + "> not present");
		}
	}

	/**
	 * Assert that an element is <b>NOT</b> present on the page
	 * 
	 * @param by
	 *            Element locator
	 */
	public void assertElementNotPresent(final By by) {
		getLogger().debug("assertElementNotPresent {}", by);
		try {
			Assert.assertFalse(isElementPresent(by));
		} catch (Error e) {
			// Nothing
		}
	}

	/**
	 * Check if an element is present on the page or not
	 * 
	 * @param by
	 *            Element locator
	 * @return true if an element is present on the page
	 */
	public boolean isElementPresent(final By by) {
		try {
			WebElement elt = this.findElement(by);
			highlightElement(elt);
			return true;
		} catch (NoSuchElementException e) {
			return false;
		} catch (Exception e) {
			return false;
		}
	}

	/**
	 * Check if an element is present on the page or not
	 * 
	 * @param by
	 *            Element locator
	 * @return true if an element is present on the page
	 */
	public boolean isElementPresentInSource(final String lookFor) {
		try {
			String source = driver.getPageSource();
			return source.contains(lookFor);
		} catch (Exception e) {
			return false;
		}
	}

	/**
	 * @return the content of an alert.
	 */
	public String closeAlertAndGetItsText() {
		getLogger().debug("closeAlertAndGetItsText");
		try {
			Alert alert = driver.switchTo().alert();
			if (acceptNextAlert) {
				alert.accept();
			} else {
				alert.dismiss();
			}
			return alert.getText();
		} finally {
			acceptNextAlert = true;
		}
	}

	/**
	 * 
	 * @param url
	 */
	public void goTo(final String url) {
		getLogger().info("goTo {}", url);
		goTo(url, null, false);
	}

	/**
	 * 
	 * @param url
	 * @param title
	 * @param check
	 */
	public void goTo(final String url, final String title, final boolean check) {

		WebDriverWait wait = new WebDriverWait(driver, 1);

		// Wait 10sec max
		wait.until(new ExpectedCondition<Boolean>() {
			@Override
			public Boolean apply(WebDriver driver) {
				driver.get(url);
				return true;
			}
		});

		if (check) {
			checkTitle(title);
		}
	}

	/**
	 * Highlight with a yellow border an element.
	 * 
	 * @param element
	 *            to highlight
	 */
	public void highlightElement(final WebElement element) {
		if (highlighted) {
			JavascriptExecutor js = driver;
			js.executeScript(
					"arguments[0].setAttribute('style', arguments[1]);",
					element, "color: yellow; border: 2px solid yellow;");

			try {
				Thread.sleep(STEP_WAIT_TIME);
			} catch (InterruptedException e) {
				// Nothing
			}

			js.executeScript(
					"arguments[0].setAttribute('style', arguments[1]);",
					element, "");
		}
	}

	/**
	 * 
	 * Highlight with a red border an element.
	 * 
	 * @param element
	 *            to highlight
	 */
	public void highlightElementRed(final WebElement element) {
		if (highlighted) {
			JavascriptExecutor js = driver;
			js.executeScript(
					"arguments[0].setAttribute('style', arguments[1]);",
					element, "color: red; border: 2px solid red;");

			try {
				Thread.sleep(STEP_WAIT_TIME);
			} catch (InterruptedException e) {
				// Nothing
			}

			js.executeScript(
					"arguments[0].setAttribute('style', arguments[1]);",
					element, "");
		}
	}

	/**
	 * 
	 * @param by
	 * @param value
	 */
	public void typeValue(final By by, final String value) {
		getLogger().debug("typeValue {} {}", by, value);
		WebElement element = this.findElement(by);
		element.clear();
		element.sendKeys(value);
	}

	/**
	 * Type a value in a field.
	 * 
	 * @param by
	 *            Element locator
	 * @param value
	 *            to type
	 */
	public void selectValue(final By by, final String value) {
		getLogger().debug("selectValue {} {}", by, value);
		WebElement element = this.findElement(by);
		new Select(element).selectByVisibleText(value);
	}

	/**
	 * Check web element value.
	 * 
	 * @param by
	 *            element locator
	 * @param value
	 *            to check.
	 */
	public void checkValue(final By by, final String value) {
		getLogger().debug("checkValue {} {}", by, value);
		try {
			WebElement element = this.findElement(by);
			assertEquals(value, element.getAttribute("value"));
		} catch (Error e) {
			verificationErrors.append(e.toString());
		}
	}

	/**
	 * Wait until an element is detected on the page (or timeout)
	 * 
	 * @param by
	 *            element locator
	 * @throws InterruptedException
	 */
	public void waitForElementPresent(final By by) throws InterruptedException {
		for (int second = 0;; second++) {
			if (second >= 60)
				fail("timeout");
			try {
				if (isElementPresent(by))
					break;
			} catch (Exception e) {
			}
			Thread.sleep(1000);
		}
	}

	/**
	 * @return the driver
	 */
	public WebDriver getDriver() {
		return driver;
	}

	/**
	 * Sets the driver.
	 * 
	 * @param driver
	 *            the value to set
	 */
	public void setDriver(final RemoteWebDriver driver) {
		this.driver = driver;
	}

	/**
	 * @return the baseUrl
	 */
	public String getBaseUrl() {
		return baseUrl;
	}

	/**
	 * Sets the baseUrl.
	 * 
	 * @param baseUrl
	 *            the value to set
	 */
	public void setBaseUrl(final String baseUrl) {
		this.baseUrl = baseUrl;
	}

	/**
	 * @return the acceptNextAlert
	 */
	public boolean isAcceptNextAlert() {
		return acceptNextAlert;
	}

	/**
	 * @return the verificationErrors
	 */
	public StringBuffer getVerificationErrors() {
		return verificationErrors;
	}

	/**
	 * @return the highlighted
	 */
	public boolean isHighlighted() {
		return highlighted;
	}

	/**
	 * Sets the highlighted.
	 * 
	 * @param highlighted
	 *            the value to set
	 */
	public void setHighlighted(boolean highlighted) {
		this.highlighted = highlighted;
	}

	/**
	 * @return the testName
	 */
	public String getTestName() {
		return testName;
	}

	/**
	 * Sets the testName.
	 * 
	 * @param testName
	 *            the value to set
	 */
	public void setTestName(String testName) {
		this.testName = testName;
	}

}
